Android中一直存在、一直被忽略的功能!
The following article is from 群英传 Author 徐宜生
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:没错!皇叔开了个训练营
从这些地方来看,其实Widget的使用还是比较简单的,所以本文也不准备来讲解这些基础知识,下面我们针对开发中会遇到的一些实际需求来进行分析。
1.appwidget-provider配置文件
尺寸
updatePeriodMillis
updatePeriodMillis只支持设置30分钟以上的间隔,即1800000milliseconds,这也是为了保证后台能耗,即使你设置了小于30分钟的updatePeriodMillis,它也不会生效。
对于Widget来说,updatePeriodMillis控制的是系统被动刷新Widget的频率,如果当前App是活着的,那么随时可以通过广播来修改Widget。
其它
resizeMode:拉伸的方向,可以设置为horizontal|vertical,表示两边都可以拉伸。 widgetCategory:对于现在的App来说,只能设置为home_screen了,5.0之前可以设置为锁屏,现在基本已经不用了。 widgetFeatures:这是Android12之后新加的属性,设置为reconfigurable之后,就可以直接调整Widget的尺寸,而不用像之前那样先删除旧的Widget再添加新的Widget了。
配置表
configure
通过configure属性可以配置添加Widget时的Configure Activity,这个在创建默认的Widget项目时就已经可以选择创建了,所以不多讲了,实际上就是一个简单的Activity,你可以配置一些参数,写入SP,然后在Widget中进行读取,从而实现自定义配置。
2.应用内唤起Widget的添加页面
大部分时候,我们都是通过在桌面上长按的方式来添加Widget,但是在Android API 26之后,系统提供了一个新的方式来在应用内唤起——requestPinAppWidget。
文档如下:
fun requestToPinWidget(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val appWidgetManager: AppWidgetManager? = getSystemService(context, AppWidgetManager::class.java)
appWidgetManager?.let {
val myProvider = ComponentName(context, NewAppWidget::class.java)
if (appWidgetManager.isRequestPinAppWidgetSupported) {
val pinnedWidgetCallbackIntent = Intent(context, MainGroupActivity::class.java)
val successCallback: PendingIntent = PendingIntent.getBroadcast(context, 0,
pinnedWidgetCallbackIntent, PendingIntent.FLAG_UPDATE_CURRENT)
appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
}
}
}
}
通过这种方式,就可以直接唤起Widget的添加入口,从而避免用户手动在桌面中进行添加。
3.应用内主动更新Widget
前面我们提到了,当App活着的时候,可以主动来更新Widget,而且有两种方式可以实现,一种是通过广播ACTION_APPWIDGET_UPDATE,触发Widget的update回调,从而进行更新,代码如下所示。
val manager = AppWidgetManager.getInstance(this)
val ids = manager.getAppWidgetIds(ComponentName(this, XXXWidget::class.java))
val updateIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(updateIntent)
val remoteViews = RemoteViews(context.packageName, R.layout.xxx)
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, XXXWidgetProvider::class.java)
appWidgetManager.updateAppWidget(componentName, remoteViews)
这种方式就是通过AppWidgetManager来对指定的Widget进行修改,使用新的RemoteViews来更新当前Widget。
这两种方式一种是主动替换,一种是被动刷新,具体的使用场景可以根据业务的不同来使用不同的方式。
4.应用外被
产品现在重新开始重视Widget的一个重要原因,实际上就是App内部卷不动了,Widget可以在不打开App的情况下,对App进行引流,所以,应用外的Widget更新,就是一个很重要的组成部分,Widget需要展示用户感兴趣的内容,才能触发用户的点击。
private fun scheduleUpdates(context: Context) {
val activeWidgetIds = getActiveWidgetIds(context)
if (activeWidgetIds.isNotEmpty()) {
val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
val pendingIntent = getUpdatePendingIntent(context)
context.alarmManager.set(
AlarmManager.RTC_WAKEUP,
nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
pendingIntent
)
}
}
一般来说,使用updatePeriodMillis就够了,Widget的目的是为了引流,对内容的实时性其实并不是要求的那么严格,updatePeriodMillis在大部分场景下,都是够用的。
5.多布局动态适配
由于在Android12之后,用户可以在单个Widget上进行修改,从而修改Widget当前的配置,所以,用户在拖动修改Widget的尺寸时,就需要动态去调整Widget的布局,以自动适应不同的尺寸。我们可以通过下面的方式,来进行修改。
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, widgetData: AppWidgetData) {
val views41 = RemoteViews(context.packageName, R.layout.new_app_widget41).also { updateView(it, context, appWidgetId, widgetData) }
val views42 = RemoteViews(context.packageName, R.layout.new_app_widget42).also { updateView(it, context, appWidgetId, widgetData) }
val views21 = RemoteViews(context.packageName, R.layout.new_app_widget21).also { updateView(it, context, appWidgetId, widgetData) }
val viewMapping: Map<SizeF, RemoteViews> = mapOf(
SizeF(180f, 110f) to views21,
SizeF(270f, 110f) to views41,
SizeF(270f, 280f) to views42
)
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
}
private fun updateView(remoteViews: RemoteViews, context: Context, appWidgetId: Int, widgetData: AppWidgetData) {
remoteViews.setTextViewText(R.id.xxx, widgetData.xxx)
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
val rows: Int = getWidgetCellsM(minHeight)
val columns: Int = getWidgetCellsN(minWidth)
updateAppWidget(context, appWidgetManager, appWidgetId, rows, columns)
}
fun getWidgetCellsN(size: Int): Int {
var n = 2
while (73 * n - 16 < size) {
++n
}
return n - 1
}
fun getWidgetCellsM(size: Int): Int {
var m = 2
while (118 * m - 16 < size) {
++m
}
return m - 1
}
也正是因为这样的问题,如果不是只在Android 12+的设备上使用,那么通常都是固定好Widget的大小,避免使用动态布局,这也是没办法的权衡之举。
6.RemoteViews行为
RemoteViews不像普通的View,所以我们不能像写普通布局的方式一样来操纵View,但RemoteViews提供了一些set方法来帮助我们对RemoteViews中的View进行修改,例如下面的代码。
remoteViews.setTextViewText(R.id.title, widgetData.xxx)
val intentUpdate = Intent(context, XXXAppWidget::class.java).also {
it.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
}
val pendingUpdate = PendingIntent.getBroadcast(
context, appWidgetId, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.btn, pendingUpdate)
RemoteViews通常用在通知和Widget中,分别通过NotificationManager和AppWidgetManager来进行管理,它们则是通过Binder来和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信,所以,RemoteViews实际上是运行在SystemServer中的,我们在修改RemoteViews时,就需要进行跨进程通信了,而RemoteViews封装了一系列跨进程通信的方法,简化了我们的调用,这也是为什么RemoteViews不支持全部的View方法的原因,RemoteViews抽象了一系列的set方法,并将它们抽象为统一的Action接口,这样就可以提供跨进程通信的效率,同时精简核心的功能。
7.如何进行后台请求
class AppWidgetRequestService : Service() {
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val appWidgetManager = AppWidgetManager.getInstance(this)
val allWidgetIds = intent?.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
if (allWidgetIds != null) {
for (appWidgetId in allWidgetIds) {
BackgroundRequest.getWidgetData {
NewAppWidget.updateAppWidget(this, appWidgetManager, appWidgetId, AppWidgetData(book1Cover = it))
}
}
}
return super.onStartCommand(intent, flags, startId)
}
}
object BackgroundRequest : CoroutineScope by MainScope() {
fun getWidgetData(onSuccess: (result: String) -> Unit) {
launch(Dispatchers.IO) {
val response = RetrofitClient.getXXXApi().getXXXX()
if (response.isSuccess) {
onSuccess(response.data.toString())
}
}
}
}
class NewAppWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
val intent = Intent(context.applicationContext, AppWidgetRequestService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
context.startForegroundService(intent)
}
}
8.动画
https://juejin.cn/post/7048623673892143140
为了防止失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号👇